/*
 * Copyright (C) 2007  Vianney le Clément
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package fsahoraires.cours;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Occupation {
	private Cours cours;
	private String infos;
	private String[] locaux;
	private Semaines semaines;

	private static final Pattern OCCUPATION_PATTERN = Pattern
			.compile("([a-zA-Z]{3,4}[0-9]{4})(?:\\s*([^()]*))?(?:\\(.*\\))?\\s*((?:<[0-9]+-[0-9]+>)+)(.*)");
	private static final Pattern SEMAINES_PATTERN = Pattern
			.compile("<([0-9]+)-([0-9]+)>");

	public static final int NUM_SEMAINES = 14;

	/**
	 * Constructeur privé pour la méthode statique <code>combine</code>.
	 */
	private Occupation() {
	}

	/**
	 * @param cours
	 * @param str le texte tel qu'affiché dans perception
	 * @throws IllegalArgumentException si str ne correspond pas
	 */
	public Occupation(String str, CoursManager manager)
			throws IllegalArgumentException {
		Matcher match = OCCUPATION_PATTERN.matcher(str);
		if (!match.find())
			throw new IllegalArgumentException(str
					+ ": occupation de cours non trouvé");
		this.cours = manager.getCours(match.group(1));
		this.infos = match.group(2);
		if (this.infos == null)
			this.infos = "";
		this.semaines = new Semaines(match.group(3));
		this.locaux = new String[] { match.group(4) };
	}

	public Occupation(Cours cours, String infos, String[] locaux,
			List<Integer> semaines) {
		if (cours == null)
			throw new IllegalArgumentException("cours ne peut pas être null");
		if (infos == null)
			throw new IllegalArgumentException("infos ne peut pas être null");
		if (locaux == null)
			throw new IllegalArgumentException("locaux ne peut pas être null");
		this.cours = cours;
		this.infos = infos;
		this.locaux = locaux;
		this.semaines = new Semaines(semaines);
	}

	public Cours getCours() {
		return cours;
	}

	public String getInfos() {
		return infos;
	}

	public String[] getLocaux() {
		return locaux;
	}

	public Semaines getSemaines() {
		return semaines;
	}

	/**
	 * Vérifie s'il existe un réel conflit dans une liste d'occupations.
	 * <p>
	 * Pour qu'il n'y ait pas de conflit, il faut que pour chaque semaine,
	 * toutes les occupations soient du même cours.
	 * 
	 * @param occupations
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static boolean isConflict(List<Occupation> occupations) {
		if (occupations.size() < 2)
			return false;
		Set<Cours>[] semaines = new Set[NUM_SEMAINES];
		for (int i = 0; i < NUM_SEMAINES; i++)
			semaines[i] = new HashSet<Cours>();
		for (Occupation o : occupations) {
			for (Integer i : o.getSemaines())
				semaines[i - 1].add(o.getCours());
		}
		for (int i = 0; i < NUM_SEMAINES; i++) {
			if (semaines[i].size() > 1)
				return true;
		}
		return false;
	}

	/**
	 * Plusieurs occupations sont-elles <em>combinables</em> en une seule?
	 * <p>
	 * Des occupations sont <em>combinables</em> si elles sont du même cours,
	 * ont les même infos et les même semaines de début et de fin. Seuls les
	 * locaux peuvent être différents.
	 * 
	 * @param occupations
	 * @return
	 */
	public static boolean isCombinable(Occupation[] occupations) {
		if (occupations.length < 2)
			return false;
		Cours cours = occupations[0].getCours();
		String infos = occupations[0].getInfos();
		Semaines semaines = occupations[0].getSemaines();
		for (int i = 1; i < occupations.length; i++) {
			if (!occupations[i].getCours().equals(cours)
					|| !occupations[i].getInfos().equals(infos)
					|| !occupations[i].getSemaines().equals(semaines))
				return false;
		}
		return true;
	}

	/**
	 * Combine plusieurs occupations en une seule.
	 * 
	 * @param occupations les occupations à combiner. La méthode
	 *            <code>isCombinable</code> doit renvoyer <code>true</code>
	 *            avec ce tableau en paramètre.
	 * @return
	 */
	public static Occupation combine(Occupation[] occupations) {
		if (occupations.length < 2)
			throw new IllegalArgumentException(
					"occupations ne sont pas combinables");
		Occupation occupation = new Occupation();
		occupation.cours = occupations[0].getCours();
		occupation.infos = occupations[0].getInfos();
		occupation.semaines = occupations[0].getSemaines();
		List<String> locaux = new ArrayList<String>();
		for (Occupation o : occupations)
			for (String l : o.getLocaux())
				locaux.add(l);
		occupation.locaux = locaux.toArray(new String[locaux.size()]);
		return occupation;
	}

	public static class Semaines implements Iterable<Integer> {
		private int semaines;

		private Semaines(List<Integer> list) {
			semaines = 0;
			for (Integer i : list)
				set(i, true);
		}

		private Semaines(String str) {
			semaines = 0;
			Matcher m = SEMAINES_PATTERN.matcher(str);
			int start = 0;
			while (m.find(start)) {
				start = m.end();
				int begin = Integer.parseInt(m.group(1));
				int finish = Integer.parseInt(m.group(2));
				for (int i = begin; i <= finish; i++)
					set(i, true);
			}
		}

		public boolean get(int semaine) {
			if (semaine < 1 || semaine > NUM_SEMAINES)
				throw new IndexOutOfBoundsException();
			return ((semaines >>> (semaine - 1)) & 0x1) != 0;
		}

		private void set(int semaine, boolean bit) {
			if (semaine < 1 || semaine > NUM_SEMAINES)
				throw new IndexOutOfBoundsException();
			if (bit)
				semaines |= (1 << (semaine - 1));
			else
				semaines &= ~(1 << (semaine - 1));
		}

		public boolean equals(Object obj) {
			if (obj == null)
				return false;
			if (!(obj instanceof Semaines))
				return false;
			return semaines == ((Semaines) obj).semaines;
		}

		public String toString() {
			StringBuilder str = new StringBuilder();
			boolean previous = false;
			for (int i = 1; i <= NUM_SEMAINES + 1; i++) {
				if (i <= NUM_SEMAINES && get(i)) {
					if (!previous)
						str.append('<').append(i).append('-');
					previous = true;
				} else {
					if (previous)
						str.append(i - 1).append('>');
					previous = false;
				}
			}
			return str.toString();
		}

		public Iterator<Integer> iterator() {
			return new Iterator<Integer>() {
				int current = 0;
				int semaines = Semaines.this.semaines;

				public void remove() {
					throw new UnsupportedOperationException();
				}

				public Integer next() {
					if (semaines == 0)
						throw new NoSuchElementException();
					current++;
					while ((semaines & 0x1) == 0) {
						current++;
						semaines >>>= 1;
					}
					semaines >>>= 1;
					return current;
				}

				public boolean hasNext() {
					return semaines != 0;
				}
			};
		}
	}
}